"""
helpers.py
==========
General Helper Utilities

Provides:
- Timer for benchmarking
- Batch processing utilities
- Formatting helpers
- Data validation

Author: Neo4j Tutorial
Date: January 2025
"""

import time
from typing import List, Dict, Any, Callable, Optional, Generator, TypeVar, Iterator
from dataclasses import dataclass
from contextlib import contextmanager
import json
from datetime import datetime, date
from functools import wraps

T = TypeVar('T')


@contextmanager
def Timer(description: str = "Operation"):
    """
    Context manager for timing operations.
    
    Usage:
        with Timer("My operation"):
            do_something()
    """
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"   ⏱️  {description}: {elapsed*1000:.2f}ms")


def timer_decorator(func: Callable) -> Callable:
    """
    Decorator for timing function execution.
    
    Usage:
        @timer_decorator
        def my_function():
            pass
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"   ⏱️  {func.__name__}: {elapsed*1000:.2f}ms")
        return result
    return wrapper


class BatchProcessor:
    """
    Utility for processing data in batches.
    """
    
    def __init__(
        self,
        batch_size: int = 1000,
        show_progress: bool = True
    ):
        """
        Initialize batch processor.
        
        Args:
            batch_size: Number of items per batch
            show_progress: Show progress information
        """
        self.batch_size = batch_size
        self.show_progress = show_progress
    
    def batch_items(self, items: List[T]) -> Generator[List[T], None, None]:
        """
        Split items into batches.
        
        Args:
            items: List of items to batch
            
        Yields:
            Batches of items
        """
        total = len(items)
        for i in range(0, total, self.batch_size):
            batch = items[i:i + self.batch_size]
            if self.show_progress:
                end = min(i + self.batch_size, total)
                print(f"   Processing batch {i//self.batch_size + 1}: items {i+1}-{end} of {total}")
            yield batch
    
    def process_batches(
        self,
        items: List[T],
        processor: Callable[[List[T]], Any]
    ) -> List[Any]:
        """
        Process items in batches with a processor function.
        
        Args:
            items: Items to process
            processor: Function to process each batch
            
        Returns:
            List of results from each batch
        """
        results = []
        for batch in self.batch_items(items):
            result = processor(batch)
            results.append(result)
        return results


def format_cypher_result(result: Dict[str, Any], indent: int = 2) -> str:
    """
    Format a Cypher query result for display.
    
    Args:
        result: Query result dictionary
        indent: Indentation spaces
        
    Returns:
        Formatted string
    """
    def serialize(obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        elif hasattr(obj, '__dict__'):
            return obj.__dict__
        return str(obj)
    
    return json.dumps(result, indent=indent, default=serialize)


def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str:
    """
    Truncate text to maximum length.
    
    Args:
        text: Input text
        max_length: Maximum length
        suffix: Suffix for truncated text
        
    Returns:
        Truncated text
    """
    if len(text) <= max_length:
        return text
    return text[:max_length - len(suffix)] + suffix


def chunk_text(
    text: str,
    chunk_size: int = 500,
    overlap: int = 50,
    separator: str = " "
) -> List[str]:
    """
    Split text into overlapping chunks.
    
    Args:
        text: Input text
        chunk_size: Target chunk size in characters
        overlap: Overlap between chunks
        separator: Word separator
        
    Returns:
        List of text chunks
    """
    if len(text) <= chunk_size:
        return [text]
    
    words = text.split(separator)
    chunks = []
    current_chunk = []
    current_length = 0
    
    for word in words:
        word_length = len(word) + 1  # +1 for separator
        
        if current_length + word_length > chunk_size and current_chunk:
            # Save current chunk
            chunks.append(separator.join(current_chunk))
            
            # Calculate overlap words
            overlap_words = []
            overlap_length = 0
            for w in reversed(current_chunk):
                if overlap_length + len(w) + 1 > overlap:
                    break
                overlap_words.insert(0, w)
                overlap_length += len(w) + 1
            
            current_chunk = overlap_words
            current_length = overlap_length
        
        current_chunk.append(word)
        current_length += word_length
    
    # Add final chunk
    if current_chunk:
        chunks.append(separator.join(current_chunk))
    
    return chunks


def sanitize_property_name(name: str) -> str:
    """
    Sanitize a string for use as a Neo4j property name.
    
    Args:
        name: Input string
        
    Returns:
        Valid property name
    """
    # Replace invalid characters
    sanitized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
    
    # Ensure it doesn't start with a number
    if sanitized and sanitized[0].isdigit():
        sanitized = '_' + sanitized
    
    return sanitized


def validate_id(value: str, allow_empty: bool = False) -> bool:
    """
    Validate an identifier string.
    
    Args:
        value: ID value to validate
        allow_empty: Allow empty strings
        
    Returns:
        True if valid
    """
    if not value and not allow_empty:
        return False
    if not value:
        return allow_empty
    
    # Check for valid characters
    return all(c.isalnum() or c in '_-.' for c in value)


def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
    """
    Merge multiple dictionaries, later values override earlier ones.
    
    Args:
        *dicts: Dictionaries to merge
        
    Returns:
        Merged dictionary
    """
    result = {}
    for d in dicts:
        if d:
            result.update(d)
    return result


def flatten_dict(
    d: Dict[str, Any],
    parent_key: str = '',
    sep: str = '_'
) -> Dict[str, Any]:
    """
    Flatten a nested dictionary.
    
    Args:
        d: Nested dictionary
        parent_key: Parent key prefix
        sep: Key separator
        
    Returns:
        Flattened dictionary
    """
    items = []
    for k, v in d.items():
        new_key = f"{parent_key}{sep}{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(flatten_dict(v, new_key, sep).items())
        else:
            items.append((new_key, v))
    return dict(items)


@dataclass
class QueryStats:
    """Statistics for query execution."""
    query: str
    execution_time_ms: float
    rows_returned: int
    db_hits: Optional[int] = None
    
    def __str__(self) -> str:
        return (
            f"Query executed in {self.execution_time_ms:.2f}ms, "
            f"{self.rows_returned} rows returned"
        )


def retry_with_backoff(
    func: Callable,
    max_retries: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 30.0,
    exceptions: tuple = (Exception,)
) -> Any:
    """
    Retry a function with exponential backoff.
    
    Args:
        func: Function to retry
        max_retries: Maximum retry attempts
        base_delay: Initial delay in seconds
        max_delay: Maximum delay in seconds
        exceptions: Exceptions to catch and retry
        
    Returns:
        Function result
    """
    delay = base_delay
    last_exception = None
    
    for attempt in range(max_retries + 1):
        try:
            return func()
        except exceptions as e:
            last_exception = e
            if attempt < max_retries:
                print(f"   Attempt {attempt + 1} failed: {e}. Retrying in {delay:.1f}s...")
                time.sleep(delay)
                delay = min(delay * 2, max_delay)
    
    raise last_exception


def create_progress_bar(current: int, total: int, width: int = 40) -> str:
    """
    Create a text progress bar.
    
    Args:
        current: Current progress
        total: Total items
        width: Bar width in characters
        
    Returns:
        Progress bar string
    """
    if total == 0:
        percent = 100
    else:
        percent = current / total * 100
    
    filled = int(width * current / total) if total > 0 else width
    bar = '█' * filled + '░' * (width - filled)
    return f"[{bar}] {percent:.1f}% ({current}/{total})"


# ============================================================
# TESTING
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("Testing Helper Utilities")
    print("=" * 60)
    
    # Test Timer
    print("\n📌 Timer Test")
    with Timer("Sleep test"):
        time.sleep(0.1)
    
    # Test BatchProcessor
    print("\n📌 BatchProcessor Test")
    processor = BatchProcessor(batch_size=3, show_progress=True)
    items = list(range(10))
    batches = list(processor.batch_items(items))
    print(f"   Created {len(batches)} batches from {len(items)} items")
    
    # Test chunk_text
    print("\n📌 Text Chunking Test")
    long_text = "The quick brown fox jumps over the lazy dog. " * 10
    chunks = chunk_text(long_text, chunk_size=100, overlap=20)
    print(f"   Created {len(chunks)} chunks from {len(long_text)} chars")
    for i, chunk in enumerate(chunks[:3]):
        print(f"   Chunk {i+1}: {truncate_text(chunk, 50)}")
    
    # Test flatten_dict
    print("\n📌 Dictionary Flattening Test")
    nested = {
        "patient": {
            "name": "John",
            "address": {
                "city": "NYC",
                "zip": "10001"
            }
        }
    }
    flat = flatten_dict(nested)
    print(f"   Nested: {nested}")
    print(f"   Flattened: {flat}")
    
    # Test progress bar
    print("\n📌 Progress Bar Test")
    for i in range(0, 101, 25):
        print(f"   {create_progress_bar(i, 100)}")
    
    print("\n✅ Helper tests complete!")
